首页 > 技术文章 > 企业微信点餐系统读后感

zyy1688 2018-10-17 15:22 原文

1:架构示例图

2:mysql中utf8和utf8mb4区别

    那上面说了既然utf8能够存下大部分中文汉字,那为什么还要使用utf8mb4呢? 原来mysql支持的 utf8 编码最大字符长度为 3 字节,如果遇到 4 字节的宽字符就会插入异常了。三个字节的 UTF-8 最大能编码的 Unicode 字符是 0xffff,也就是 Unicode 中的基本多文种平面(BMP)。也就是说,任何不在基本多文本平面的 Unicode字符,都无法使用 Mysql 的 utf8 字符集存储。包括 Emoji 表情(Emoji 是一种特殊的 Unicode 编码,常见于 ios 和 android 手机上),和很多不常用的汉字,以及任何新增的 Unicode 字符等等。

 

3:数据库连接包

<!-- 操纵数据库的工具 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!--Mysql数据库连接包-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

4:jpa访问数据库的日志打印配置
# 打印SQL
show-sql: true

 5:数据库对象实体配置信息

//@Table(name = "product_category")//如果类名和表名不是一致的话就需要
@Entity
@DynamicUpdate //动态更新-需要设置数据库的更新时间字段为自动更新 这样,查询出时间,去设置其他字段后保存,更新时间依然会更新
@Data //不用写setter和getter方法,toString也可以省了 性能是一样的,可以去看编译的class文件,和我们写的一样
//@Getter //不用写getter方法
//@Setter //不用写setter方法
public class ProductCategory {
/**
* 类目ID
*/
@Id//主键id
@GeneratedValue //自增
private Integer categoryId;

//    /**
// * 关联订单详情
// */
// @Transient //在数据库对应的时候能忽略,就不会去数据库找对应的字段了
// private List<OrderDetail> orderDetailList;
/**
* 订单状态,默认为新下单
*/
private Integer orderStatus = OrderStausEnum.NEW.getCode();
//如果有构造方法必须添加一个默认的
public ProductCategory() {
}

public ProductCategory(String categoryName, Integer categoryType) {
this.categoryName = categoryName;
this.categoryType = categoryType;
}



}
6:jpa访问数据库
public interface ProductCategoryDao extends JpaRepository<ProductCategory,Integer>{//后面的是主键类型

/**
* 通过categoryType集合 查询出ProductCategory集合
* @param categoryTypeList
* @return
*/
List<ProductCategory> findByCategoryTypeIn(List<Integer> categoryTypeList);
}
public interface OrderDetailDao extends JpaRepository<OrderDetail,String> {

/**
* 查询订单详情 - 根据订单id
* @param orderId
* @return
*/
List<OrderDetail> findByOrderId(String orderId);

}

7:lombok 使用
<!--
实体类使用@Data注解 不用再写setter、getter方法
类引入日志 使用@Slf4j注解
IDEA记得装lombok插件
-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

8: 调试断言应用

@Test
@Transactional //在测试里面的事务是完全回滚,运行完就回滚
public void saveTest(){
ProductCategory productCategory = new ProductCategory("女生最爱",5);
ProductCategory result = productCategoryDao.save(productCategory);
Assert.assertNotNull(result);
// Assert.assertNotEquals(null,result);//不期望是null,期望是result 和上面是一样的
//写得统一一点的话,可以这样
//Assert.assertTrue("查询所有的订单列表应该大于0",orderDTOPage.getTotalElements()>0);//查询总数大于0

}

@Test
public void findOne() throws Exception {
ProductCategory productCategory = productCategoryService.findOne(1);
Assert.assertEquals(new Integer(1),productCategory.getCategoryId());
}

@Test
public void findAll() throws Exception {
List<ProductCategory> productCategoryList = productCategoryService.findAll();
Assert.assertNotEquals(0,productCategoryList.size());
}


9:Arrays.asList 方法
List<Integer> list = Arrays.asList(2,3,4);
List<ProductInfoVO> productInfoVOList = new ArrayList<>();

Map<String,String> map = new HashMap<>();
map.put("orderId",createResult.getOrderId());

10:测试选择注入的类

@RunWith(SpringRunner.class)
@SpringBootTest
public class ProductCategoryServiceImplTest {

@Autowired
private ProductCategoryServiceImpl productCategoryService;//这里选择实现,因为是实现的测试
}
11:分页

@GetMapping("/list")
public ModelAndView list(@RequestParam(value = "page",defaultValue = "1")Integer page,
@RequestParam(value = "size",defaultValue = "10")Integer size,
Map<String,Object> map){//map - 模板数据返回到页面
PageRequest pageRequest = new PageRequest(page-1,size);
Page<OrderDTO> orderDTOPage = orderService.findList(pageRequest);
map.put("orderDTOPage",orderDTOPage);
map.put("currentPage",page);//当前页
map.put("size",size);//一页有多少数据
return new ModelAndView("order/list",map);
}



Page<OrderMaster> findByBuyerOpenid(String buyerOpenid, Pageable pageable);
@Test
public void findList() throws Exception {
PageRequest pageRequest = new PageRequest(0,2);
Page<OrderDTO> orderDTOPage = orderService.findList(BUYER_OPENID,pageRequest);
log.info("list = {}",orderDTOPage.getContent());
Assert.assertNotEquals(0,orderDTOPage.getTotalElements());
}

@Override
public Page<OrderDTO> findList(Pageable pageable) {
Page<OrderMaster> orderMasterPage =orderMasterDao.findAll(pageable);//分页查询
List<OrderDTO> orderDTOList = OrderMaster2OrderDTOConverter.convert(orderMasterPage.getContent());
return new PageImpl<>(orderDTOList,pageable,orderMasterPage.getTotalElements());
}


12:枚举的使用
@Getter
public enum OrderStausEnum implements CodeEnum<Integer> {
NEW(0,"新订单"),
FINISHED(1,"完结"),
CANCEL(2,"已取消"),
;
private Integer code;

private String message;

OrderStausEnum(Integer code, String message) {
this.code = code;
this.message = message;
}

/**
* 通过cide获取订单状态枚举
* 不能这么写,如果还有另外的枚举状态,就还得复制拷贝过去
* 所以用实现接口和工具类来实现
* @param code
* @return
*/
// public static OrderStausEnum getOrderStatusEnum(Integer code){
// for(OrderStausEnum orderStausEnum:OrderStausEnum.values()){
// if(orderStausEnum.getCode().equals(code)){
// return orderStausEnum;
// }
// }
// return null;
// }

}


public class ProductInfo
{
/**
* 上架下架
* 注意是公开方法
* @return
*/
@JsonIgnore //返回json格式的字符串时,忽略该字段/方法 也就是不序列化
public ProductStatusEnum getProductStatusEnum(){
return EnumUtil.getByCode(productStatus,ProductStatusEnum.class);
}
}
public interface CodeEnum<T> {
T getCode();
}

public class EnumUtil {
/**
* 通过code和枚举类型获取枚举
* @param code code
* @param enumClass 枚举类型class
* @param <T> 枚举类型
* @return
*/
public static <T extends CodeEnum> T getByCode(Integer code, Class<T> enumClass){
for(T each: enumClass.getEnumConstants()){//遍历枚举类型
if(each.getCode().equals(code)){
return each;
}
}
return null;
}
}




13:配置Url项目名 也就是URl的前缀
#server:
# 配置Url项目名 也就是URl的前缀
# context-path: /sell
14:vo 包 entity包 dataobject 包 form 包
15:返回对象实体
@Data
//@JsonInclude(JsonInclude.Include.NON_NULL) //为null的属性不返回到前端, 也就是类转json 属性为NULL的不参加序列化
public class ResultVO<T> implements Serializable{
private static final long serialVersionUID = 4176441568338187710L;//实现序列化
/**
* 错误码
*/
private Integer code;
/**
* 提示信息
*/
private String msg;
// private String msg = "";//赋予初始值
//private List<OrderDetail> orderDetailList = new ArrayList<>();//赋予初始值


/**
* 返回的具体内容
*/
private T data;
}

16:chrome 安装jsonview 插件
17:JsonProperty 会返回json 对应的字段名
@Data
public class ProductInfoVO implements Serializable{
private static final long serialVersionUID = 4177439763246797991L;
@JsonProperty("id")
private String productId;
@JsonProperty("name")
private String productName;

}
18:resultVo 静态方法
public class ResultVOUtil {


public static ResultVO success(Object object){
ResultVO resultVO = new ResultVO();
resultVO.setData(object);
resultVO.setCode(0);
resultVO.setMsg("成功");

return resultVO;
}

public static ResultVO success(){
return success(null);
}

public static ResultVO error(Integer code,String msg){
ResultVO resultVO = new ResultVO();
resultVO.setMsg(msg);
resultVO.setCode(code);
return resultVO;
}

}

19:异常类
@Data
public class SellException extends RuntimeException{

private Integer code;

public SellException(ResultEnum resultEnum) {
super(resultEnum.getMessage());
this.code = resultEnum.getCode();
}
public SellException(Integer code, String defaultMessage) {
super(defaultMessage);
this.code=code;
}
}

20:计算总价
BigDecimal orderAmount = new BigDecimal(BigInteger.ZERO);
orderAmount=productInfo.getProductPrice()
.multiply(new BigDecimal(orderDetail.getProductQuantity()))//相乘- 计算出一种商品的总价
.add(orderAmount);
21:生成唯一id
public class KeyUtil {

/**
* 生成唯一主键
* 格式:时间+随机数
* @return
*/
public static synchronized String getUniqueKey(){//加一个锁
Random random = new Random();
Integer number = random.nextInt(900000) + 100000;//随机六位数
return System.currentTimeMillis()+String.valueOf(number);
}
}
22:判断list 是否为空,以及复制属性
if(CollectionUtils.isEmpty(orderDetailList)){//判断orderDetailList为空
throw new SellException(ResultEnum.ORDERDETAIL_NOT_EXIST);
}
if(!StringUtils.isEmpty(productId)){
ProductInfo productInfo = productInfoService.findOne(productId);
map.put("productInfo",productInfo);
}
BeanUtils.copyProperties(orderDTO,orderMaster);//将orderDTO拷贝到orderMaster

23:gojson 使用
Gson gson = new Gson();
List<OrderDetail> orderDetailList = new ArrayList<>();
try {
orderDetailList = gson.fromJson(orderForm.getItems(),
new TypeToken<List<OrderDetail>>() {
}.getType());
}catch (Exception e){
log.error("[对象转换] 错误,json={}",orderForm.getItems());
throw new SellException(ResultEnum.PARAM_ERROR);
}

<!-- json转换 -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>

24:date加了JsonSerialize注解的参数
/**
* 创建时间
*/
@JsonSerialize(using = Date2LongSerializer.class)//让时间戳精度在秒
private Date createTime;
public class Date2LongSerializer extends JsonSerializer<Date> {//Date类型对应OrderDTO中的时间的属性类型
@Override
public void serialize(Date date, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
jsonGenerator.writeNumber(date.getTime() / 1000);//这个date是加了JsonSerialize注解的参数
}
}
25:为空或者null的时候 序列化忽略
//Include.Include.ALWAYS 默认
//Include.NON_DEFAULT 属性为默认值不序列化
//Include.NON_EMPTY 属性为 空(“”) 或者为 NULL 都不序列化
//Include.NON_NULL 属性为NULL 不序列化
//@JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL) //旧版本的,已弃用
//@JsonInclude(JsonInclude.Include.NON_NULL)//如果属性为null就不返回到前端去,也就是转json 为NULL不参加序列化
public class OrderDTO {
}

或者配置文件中
# 为null的属性不序列化成json字符串 - 也就是前端不会看到为null的属性
#  jackson:
# default-property-inclusion: non_null

26:前端页面数据处理
<strong>${msg!"操作成功"}</strong>
<input hidden type="text" name="categoryId" value="${(productCategory.categoryType)!''}">
<#list 1..orderDTOPage.getTotalPages() as index>
<#if currentPage == index>
<li class="disabled"><a href="#">${index}</a></li>
<#elseif (index-currentPage lte 2 && index-currentPage gte -2) || (currentPage lte 2 && index lte 5) || (orderDTOPage.getTotalPages()-currentPage lte 2 && orderDTOPage.getTotalPages()-index lt 5)>
<li><a href="/seller/order/list?page=${index}&size=${size}">${index}</a></li>
</#if>
</#list>
<#--当前页大于等于总页数-->
<#if currentPage gte orderDTOPage.getTotalPages()>
<li class="disabled"><a href="#">下一页</a></li>
<#else>

<select name="categoryType" class="form-control">
<#list productCategoryList as productCategory>
<#--商品类目存在 (productInfo.categoryType)?? -->
<option value="${productCategory.categoryType}"
<#if (productInfo.categoryType)?? && productInfo.categoryType==productCategory.categoryType>
selected
</#if>
>
${productCategory.categoryName}
</option>
</#list>
</select>


27:分布式系统
1:多节点2:消息通信3:不共享内存
集群
1:多节点2:不共享内存
分布式计算
1:多节点2:消息通信
28:redis 使用
<!-- 引入redis依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
redis:
host: 127.0.0.1
port: 6379
password: chenhaoxiang

 * Explain: Redis常量
*/
public interface RedisConstans {
/**
* token前缀
*/
String TOKEN_PREFIX="token_%s";
/**
* 过期时间
*/
Integer EXPIPE = 7200; //单位s

}


@Autowired
private StringRedisTemplate stringRedisTemplate;


    @GetMapping("/login")
public ModelAndView login(@RequestParam("openid") String openid,
HttpServletResponse response,
Map<String,Object> map){
//1.openid去和数据库里的数据匹配
SellerInfo sellerInfo = sellerService.findSellerInfoByOpenid(openid);
//TODO 未考虑新增的情况,也就是用户微信扫码登录不存在openid的情况下
if(sellerInfo==null){
map.put("msg", ResultEnum.LOGIN_FAIL.getMessage());
map.put("url","/seller/order/list");
return new ModelAndView("common/error",map);
}
//2.设置token至Redis
// stringRedisTemplate.opsForValue().set("abc","122");//操作某些value 写入key-value
String token= UUID.randomUUID().toString();
stringRedisTemplate.opsForValue().set(String.format(RedisConstans.TOKEN_PREFIX,token),openid,RedisConstans.EXPIPE, TimeUnit.SECONDS);//key,value,过期时间,时间单位 s

//3.设置token至cookie
CookieUtil.set(response, CookieConstant.TOKEN,token,CookieConstant.EXPIPE);
//做一个跳转获取订单列表后再跳转 重定向不要带项目名 - 最好带绝对地址 也就是带http://的绝对地址
return new ModelAndView("redirect:"+projectUrlConfig.getProject()+"/seller/order/list");
}


@GetMapping("/logout")
public ModelAndView logout(HttpServletRequest request,
HttpServletResponse response,
Map<String,Object> map){
//1.从Cookie查询
Cookie cookie =CookieUtil.get(request,CookieConstant.TOKEN);
if(cookie!=null){
//2.清除redis
stringRedisTemplate.opsForValue().getOperations().delete(String.format(RedisConstans.TOKEN_PREFIX,cookie.getValue()));
//3.清除cookie
CookieUtil.del(response,CookieConstant.TOKEN);
}
map.put("msg",ResultEnum.LOGOUT_SUCCESS.getMessage());
map.put("url","/seller/order/list");
return new ModelAndView("common/success",map);
}




29:cookie 相关操作类
public class CookieUtil {

/**
* 清除cookie
* @param response
* @param name
*/
public static void del(HttpServletResponse response,
String name){
set(response,name,null,0);
}

/**
* 设置cookie
* @param response
* @param name
* @param value
* @param maxAge
*/
public static void set(HttpServletResponse response,
String name,
String value,
int maxAge){
Cookie cookie = new Cookie(name,value);
cookie.setPath("/");
cookie.setMaxAge(maxAge);
response.addCookie(cookie);
}

/**
* 获取Cookie
* @param request
* @param name
* @return
*/
public static Cookie get(HttpServletRequest request,
String name){
Map<String,Cookie> cookieMap =readCookieMap(request);
if(cookieMap.containsKey(name)){
return cookieMap.get(name);
}
return null;
}

/**
* 将cookie数组封装为Map
* @param request
* @return
*/
private static Map<String,Cookie> readCookieMap(HttpServletRequest request){
Map<String,Cookie> cookieMap = new HashMap<>();
Cookie[] cookies =request.getCookies();
if(cookies!=null){
for(Cookie cookie:cookies){
cookieMap.put(cookie.getName(),cookie);
}
}
return cookieMap;
}

}

30:加入权限切面
@Aspect
@Component
@Slf4j
public class SellerAuthorizeAspect {

@Autowired
private StringRedisTemplate redisTemplate;

@Pointcut("execution(public * com.imooc.controller.Seller*.*(..))" +
"&& !execution(public * com.imooc.controller.SellerUserController.*(..))")
public void verify() {}

@Before("verify()")
public void doVerify() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();

//查询cookie
Cookie cookie = CookieUtil.get(request, CookieConstant.TOKEN);
if (cookie == null) {
log.warn("【登录校验】Cookie中查不到token");
throw new SellerAuthorizeException();
}

//去redis里查询
String tokenValue = redisTemplate.opsForValue().get(String.format(RedisConstant.TOKEN_PREFIX, cookie.getValue()));
if (StringUtils.isEmpty(tokenValue)) {
log.warn("【登录校验】Redis中查不到token");
throw new SellerAuthorizeException();
}
}

}

31:拦截系统异常 并进行统一处理

@ControllerAdvice
public class SellExceptionHandler {

@Autowired
private ProjectUrlConfig projectUrlConfig;
/**
* 全局异常捕捉处理
* 拦截登录异常
* 重定向至登录页面 - 也就是微信扫码登录
* @return
*/
@ExceptionHandler(value = SellAuthorizeException.class)
public ModelAndView handlerSellerAuthorizeException(){
return new ModelAndView("redirect:"
.concat(projectUrlConfig.getWechatOpenAuthorize())//微信开放平台登录授权地址
.concat("/wechat/qrAuthorize")
.concat("?returnUrl=")
.concat(projectUrlConfig.getProject())//服务器访问的地址
.concat("/seller/login"));
}

/**
* 全局异常捕捉处理
* @return
*/
@ExceptionHandler(value = SellException.class)
@ResponseBody
public ResultVO handlerSellException(SellException e){
return ResultVOUtil.error(e.getCode(),e.getMessage());
}

/**
* 全局异常捕捉处理
* 返回状态码的修改 不再返回200
* @return
*/
@ExceptionHandler(value = ResponseBankException.class)
@ResponseStatus(HttpStatus.FORBIDDEN) //返回状态码的修改
@ResponseBody
public ResultVO handlerResponseBankException(ResponseBankException e){
return ResultVOUtil.error(e.getCode(),e.getMessage());
}

}
32:projectUrlConfig配置类
@Data
@ConfigurationProperties(prefix = "projectUrl")
@Component
public class ProjectUrlConfig {

/**
* 微信公众平台授权url
*/
public String wechatMpAuthorize;

/**
* 微信开放平台授权url
*/
public String wechatOpenAuthorize;

/**
* 点餐系统
*/
public String sell;
}
33:集成mybatis的依赖-
<!--集成mybatis的依赖-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>

34:mybatis配置文件
@SpringBootApplication
//@MapperScan(basePackages = {"cn.chenhaoxiang.dataObject.mapper"})//配置mybatis mapper扫描路径
@EnableCaching //缓存支持 配置Redis缓存需要的
public class SellApplication {
public static void main(String[] args) {
SpringApplication.run(SellApplication.class, args);
}
}

35:mapper相关文件
//在SellApplication上配置了注解@MapperScan(basePackages = "cn.chenhaoxiang.dataObject.mapper")//配置mybatis mapper扫描路径 所以不用我们再写注解注入Bean
@Mapper
@Component //也可以通过上面两个注解实现注入Bean
public interface ProductCategoryMapper {
/**
* 通过Map插入
* @param map
* @return
*/
@Insert("insert into product_category(category_name,category_type) values (#{category_name,jdbcType=VARCHAR},#{category_type,jdbcType=INTEGER})")
int insertByMap(Map<String,Object> map);

/**
* 通过对象插入
* @param productCategory
* @return
*/
@Insert("insert into product_category(category_name,category_type) values (#{categoryName,jdbcType=VARCHAR},#{categoryType,jdbcType=INTEGER})")
int insertByObject(ProductCategory productCategory);

/**
* 通过CategoryType查询
* @param categoryType
* @return
*/
@Select("select * from product_category where category_type=#{categoryType,jdbcType=INTEGER}")
@Results({
@Result(column = "category_id",property = "categoryId"),
@Result(column = "category_name",property = "categoryName"),
@Result(column = "category_type",property = "categoryType"),
@Result(column = "create_time",property = "createTime"),
})//映射
ProductCategory findByCategoryType(Integer categoryType);

/**
* 通过CategoryName查询多条数据
* @param categoryName
* @return
*/
@Select("select * from product_category where category_name=#{categoryName}")
@Results({
@Result(column = "category_id",property = "categoryId"),
@Result(column = "category_name",property = "categoryName"),
@Result(column = "category_type",property = "categoryType"),
@Result(column = "create_time",property = "createTime"),
})//映射
List<ProductCategory> findByCategoryName(String categoryName);

/**
* 通过categoryType修改categoryName
* @param categoryType
* @param categoryName
* @return
*/
@Update("update product_category set category_name=#{categoryName} where category_type=#{categoryType}")
int updateByCategoryType(@Param("categoryType")Integer categoryType,@Param("categoryName")String categoryName);

/**
* 通过categoryType修改对象category_name
* @param productCategory
* @return
*/
@Update("update product_category set category_name=#{categoryName} where category_type=#{categoryType}")
int updateByObject(ProductCategory productCategory);

/**
* 删除
* @param categoryType
* @return
*/
@Delete("delete from product_category where category_type=#{categoryType}")
int deleteByCategoryType(Integer categoryType);

/**
* 配置xml的方式实现查询
* 配置文件的resources下面
* 在yml配置文件中配置xml的扫描路径
* xml的写法可以百度一下,spring集成mybatis的,现在很多人的写法都是这种xml的。包括我自己,哈哈,以后的项目会用注解方式了
* mybatis官方不推荐这种写法噢
* @param categoryType
* @return
*/
ProductCategory selectByCategoryType(Integer categoryType);
}
36:日志输出级别-debug看到执行的sql
#  设置cn.chenhaoxiang包下所有类的日志输出级别-debug能看到执行的sql
logging:
level:
cn.chenhaoxiang: debug

37:mybatis xml 配置
mybatis:
mapper-locations: classpath:mapper/*.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.chenhaoxiang.dataObject.mapper.ProductCategoryMapper">

<resultMap id="BaseResultMap" type="cn.chenhaoxiang.dataObject.ProductCategory">
<id column="category_id" property="categoryId" jdbcType="INTEGER"></id>
<id column="category_name" property="categoryName" jdbcType="VARCHAR"></id>
<id column="category_type" property="categoryType" jdbcType="INTEGER"></id>
</resultMap>

<select id="selectByCategoryType" resultMap="BaseResultMap" parameterType="java.lang.Integer">
select category_id,category_name,category_type
from product_category
where category_type=#{categoryType,jdbcType=INTEGER}
</select>

</mapper>

38:synchronized 锁
粒度不能控制很细只能在方法上面,只能是单点,不能用来分布式
redis锁刚好相反

39 :分布式锁
@Component
@Slf4j
public class RedisLock {
@Autowired
private StringRedisTemplate stringRedisTemplate;

/**
* 加锁
* @param key productId - 商品的唯一标志
* @param value 当前时间+超时时间
* @return
*/
public boolean lock(String key,String value){
if(stringRedisTemplate.opsForValue().setIfAbsent(key,value)){//对应setnx命令
//可以成功设置,也就是key不存在
return true;
}

//判断锁超时 - 防止原来的操作异常,没有运行解锁操作 防止死锁
String currentValue = stringRedisTemplate.opsForValue().get(key);
//如果锁过期
if(!StringUtils.isEmpty(currentValue) && Long.parseLong(currentValue) < System.currentTimeMillis()){//currentValue不为空且小于当前时间
//获取上一个锁的时间value
String oldValue =stringRedisTemplate.opsForValue().getAndSet(key,value);//对应getset,如果key存在

//假设两个线程同时进来,key被占用了。获取的值currentValue=A(get取的旧的值肯定是一样的),两个线程的value都是B,key都是K.锁时间已经过期了。
//而这里面的getAndSet一次只会一个执行,也就是一个执行之后,上一个的value已经变成了B。只有一个线程获取的上一个值会是A,另一个线程拿到的值是B。
if(!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue) ){
//oldValue不为空且oldValue等于currentValue,也就是校验是不是上个对应的商品时间戳,也是防止并发
return true;
}
}
return false;
}


/**
* 解锁
* @param key
* @param value
*/
public void unlock(String key,String value){
try {
String currentValue = stringRedisTemplate.opsForValue().get(key);
if(!StringUtils.isEmpty(currentValue) && currentValue.equals(value) ){
stringRedisTemplate.opsForValue().getOperations().delete(key);//删除key
}
} catch (Exception e) {
log.error("[Redis分布式锁] 解锁出现异常了,{}",e);
}
}

}

40:加减锁
public void orderProductMocckDiffUser(String productId) {//解决方法一:synchronized锁方法是可以解决的,但是请求会变慢,请求变慢是正常的。主要是没做到细粒度控制。比如有很多商品的秒杀,但是这个把所有商品的秒杀都锁住了。而且这个只适合单机的情况,不适合集群

//加锁
long time = System.currentTimeMillis() + TIMEOUT;
if(!redisLock.lock(productId,String.valueOf(time))){
throw new SellException(101,"很抱歉,人太多了,换个姿势再试试~~");
}

//1.查询该商品库存,为0则活动结束


//解锁
redisLock.unlock(productId,String.valueOf(time));

}

41:缓存机制

@SpringBootApplication
//@MapperScan(basePackages = {"cn.chenhaoxiang.dataObject.mapper"})//配置mybatis mapper扫描路径
@EnableCaching //缓存支持 配置Redis缓存需要的
public class SellApplication {
public static void main(String[] args) {
SpringApplication.run(SellApplication.class, args);
}
}

<!-- 缓存的依赖 我们不需要再引入了,在websocket中已经引入了。 ctrl+shift+alt+u 快捷键查询maven包依赖引入关系,ctrl+f搜索-->
<!--<dependency>-->
<!--<groupId>org.springframework.boot</groupId>-->
<!--<artifactId>spring-boot-start-cache</artifactId>-->
<!--</dependency>-->

方法缓存
@GetMapping("list")
@Cacheable(cacheNames = "product",key = "123",unless = "#result.getCode() != 0")
//Redis缓存注解 Cacheable第一次访问会访问到方内的内容,方法会返回一个对象,返回对象的时候,会把这个对象存储。下一次访问的时候,不会进去这个方法,直接从redis缓存中拿
//上面的key的值是可以动态写的@Cacheable(cacheNames = "product",key = "#sellerId") sellerId为方法中的参数名
//condition判断是否成立的条件 例如key = "#sellerId",condition = "#sellerId.length() > 3" 只有条件成立才会对结果缓存,结果不成立是不缓存的
//依据结果来判断是否缓存 unless = "#result.getCode() != 0",#result其实就是ResultVO,也就是返回的对象
//unless(除什么之外,如果不 的意思) 如果=0就缓存,需要写成!=0。理解起来就是,除了不等于0的情况之外,才缓存,也就是等于0才缓存。
//其实就是,你想要什么条件下缓存,你反过来写就行了
//测试缓存的话,你可以在方法内打一个断点进行测试
//注意,返回的缓存对象一定要实现序列化!!!
public ResultVO list(){
}

ResultVO 必须实现序列化
@Data
//@JsonInclude(JsonInclude.Include.NON_NULL) //为null的属性不返回到前端, 也就是类转json 属性为NULL的不参加序列化
public class ResultVO<T> implements Serializable{
private static final long serialVersionUID = 4176441568338187710L;//实现序列化
}

42:@CachePut是更新缓存
//    @CachePut(key = "123") //和上面findOne的返回对象对应
// @CachePut(cacheNames = "product",key = "123") //和上面findOne的返回对象对应
public ProductInfo save(ProductInfo productInfo) {
return productInfoDao.save(productInfo);
}

43:项目部署
两种部署方式
tomcat
java -jar
mvn clean package -Dmaven.test.skip=true
拷贝文件

 

 

启动设置参数

java -jar -Dserver.port=9999 sell.jar
java -jar -Dserver.port=9999 sell.jar

java -jar -Dserver.port=9999 -Dspring.profiles.active=prod sell.jar
linux后台运行发布:nohup java -jar jar包名 > /dev/null 2>&1 &

linux查看进程: ps -ef |grep sell.jar 

bash 命令
vim start.sh
#!/bin/sh
nohup java -jar sell.jar > /dev/null 2>&1 &

bash start.sh
kill -9 2576

系统重启 服务重启

 

cd /etc/systemd/system
vim sell.service


systemctl daemon-reload
systemctl start sell

systemctl stop sell

systemctl enable sell

systemctl disable sell

 


 

<build>
<!--打成包的包名
启动命令和指定端口: java -jar jar包名 \-\-server.port=8090
多个配置文件,指定配置文件 \-\-spring.profiles.active=prod
linux后台运行发布:nohup java -jar jar包名 > /dev/null 2>&1 &
linux查看进程: ps -ef |grep jar包名

-->
<finalName>sell</finalName>




推荐阅读