redis事件監聽及在訂單系統中的使用
https://blog.csdn.net/qq_37334135/article/details/77717248
通常在網上買好物品,或者說手機掃碼後,點選付款,這時就會向後臺傳送請求,生成訂單資訊,以及夠買商品的資訊存入到資料庫對應的表比如:訂單表和商品銷售表,或者還有物流資訊表等。簡單起見,就拿掃碼購物來說,這裡就不需要物流資訊表了,只需要訂單表,商品銷售表,而且一次只能買一個商品,對應生成一個訂單。
注:這裡用到的是spring data +redis,也用到了spring data +jpa所以前提這兩個都瞭解。
訂單表字段有:訂單id、建立時間、修改時間、付款方式、金額、支付狀態、訂單編號;
商品銷售表字段有:id、建立時間、修改時間,支付狀態,商品id(外來鍵),訂單id(外來鍵),使用者id(外來鍵);
有三個外來鍵,那麼肯定還有另外的商品表,使用者表
商品表字段:id、建立時間、修改時間、商品名稱、價格;
使用者表:id、建立時間、修改時間、郵箱、使用者名稱、密碼、電話。
所以一個涉及4張表
流程大致是:使用者點選付款,傳送請求,後臺接收到請求後,生成訂單資訊和商品銷售資訊,儲存到資料庫表中。同時把訂單資訊存入到redis中,key可以設為訂單編號,同時設定過期時間。到了過期時間後,redis監聽器監聽到了過期的key,取出該key查詢資料庫訂單表,如果發現支付狀態不是成功(使用者為付款,需要使訂單失效),那麼修改支付狀態為失敗(也就是使用者下單後一直不付款,到了一定時間後,那麼就應該讓這個訂單作廢。如果使用者付款了,在支付寶回撥的接口裡面會將支付狀態修改為成功)。
建立spring boot專案,如果用的eclipse的話最好安裝STS外掛。
1、MySql、jpa、redis配置
server.port=8081 spring.datasource.url=jdbc:mysql://localhost:3306/logistic spring.datasource.username=root spring.datasource.password=123456 spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.jpa.properties.hibernate.hbm2ddl.auto=update spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect spring.jpa.show-sql= true spring.jpa.properties.hibernate.format_sql=true spring.redis.host=192.168.25.128 spring.redis.pool.max-active=1000 spring.redis.pool.max-idle=100 spring.redis.pool.max-wait=-1 spring.redis.pool.min-idle=0 spring.redis.port=6379 spring.redis.timeout=0
2、建立好專案後編寫4個實體類
@Entity @Table(name="t_goods") public class Goods { @Id @GeneratedValue private long id; private String name; private Integer price; @Column(name="create_time") @Temporal(TemporalType.TIMESTAMP) private Date createTime; @Column(name="modified_time") @Temporal(TemporalType.TIMESTAMP) private Date modifiedTime; get、set方法 }
@Entity @Table(name = "t_user") public class User { @Id @GeneratedValue(strategy=GenerationType.IDENTITY) private Long id; @Column(name="user_name") private String userName; private String password; private String telephone; private String email; @Column(name="create_time") @Temporal(TemporalType.DATE) private Date createTime; @Column(name="modified_time") @Temporal(TemporalType.TIMESTAMP) private Date modifiedTime; get、set方法 }
@Entity @Table(name="t_order") public class Order { @Id @GeneratedValue(strategy=GenerationType.IDENTITY) private Long id; private Date createTime= new Date(); private Date modifiedTime = new Date(); //金額 private Integer payment; //0:待支付 1:支付成功 2:支付失敗 private Integer status; //支付方式 0:支付寶 1:微信 private Integer channel; //訂單編號 private String tradeNo; get、set方法 }
@Entity @Table(name="t_goods_sells") public class GoodsSell { @Id @GeneratedValue private long id; @ManyToOne private Goods goods; @OneToOne private Order order; @ManyToOne private User user; private int count; //0:待付款 1:已付款 2:未付款 private int status; @Column(name="create_time") @Temporal(TemporalType.TIMESTAMP) private Date createTime; @Column(name="modified_time") @Temporal(TemporalType.TIMESTAMP) private Date modifiedTime; get、set方法 }
3、4個Repository介面(對應4個實體類)
@Repository public interface GoodsRepository extends CrudRepository<Goods, Long>{ }
@Repository public interface UserRepository extends CrudRepository<User, Long>{ }
@Repository public interface PayOrderRepository extends JpaRepository<Order, Long>{ //根據訂單編號查詢訂單資訊 @Query(value="SELECT o FROM Order o WHERE o.tradeNo=?1") Order findOrderByTradeNo(String tradeNo); //根據訂單id修改訂單狀態 @Modifying @Query("UPDATE Order o SET o.status=?1 WHERE o.id=?2") int setStatusByOrderId(int status, long orderId); }
@Repository public interface GoodsSellsRepository extends CrudRepository<GoodsSell, Long>{ //根據商品id修改商品銷售狀態 @Modifying @Query("UPDATE GoodsSell o SET o.status=?1 WHERE o.id=?2") int setStatusByGoodsId(int status, long goodsId); //根據訂單id查詢商品銷售資訊 @Query(value="SELECT * FROM t_goods_sells t WHERE t.order_id=?1",nativeQuery=true) GoodsSell findOrderByOrderId(long orderId); }
4、訂單Redis介面和實現類
public interface OrderRedisService { public void saveOrder(String outTradeNo,OrderRedisDo redisDo); public String getOrder(String outTradeNo); public void deleteOrder(String outTradeNo); }
@Service public class OrderRedisServiceImpl implements OrderRedisService{ @Autowired private StringRedisTemplate redisTemplate; /* * 儲存訂單 */ public void saveOrder(String outTradeNo, OrderRedisDo redisDo) { String key = "order:"+outTradeNo; //key過期時間為120秒 redisTemplate.opsForValue().set(key, JsonUtils.objectToJson(redisDo), 120, TimeUnit.SECONDS); } /* * 獲取訂單 */ public String getOrder(String outTradeNo) { String key = "order:"+outTradeNo; String message = redisTemplate.opsForValue().get(key); return message; } /* * 刪除訂單 */ public void deleteOrder(String outTradeNo) { String key = "order:"+outTradeNo; redisTemplate.delete(key); } }
工具類JSONUtils如下:
public class JsonUtils { // 定義jackson物件 private static final ObjectMapper MAPPER = new ObjectMapper(); /** * 將物件轉換成json字串。 */ public static String objectToJson(Object data) { try { String string = MAPPER.writeValueAsString(data); return string; } catch (JsonProcessingException e) { e.printStackTrace(); } return null; } /** * 將json結果集轉化為物件 */ public static <T> T jsonToPojo(String jsonData, Class<T> beanType) { try { T t = MAPPER.readValue(jsonData, beanType); return t; } catch (Exception e) { e.printStackTrace(); } return null; } /** * 將json資料轉換成pojo物件list */ public static <T>List<T> jsonToList(String jsonData, Class<T> beanType) { JavaType javaType = MAPPER.getTypeFactory().constructParametricType(List.class, beanType); try { List<T> list = MAPPER.readValue(jsonData, javaType); return list; } catch (Exception e) { e.printStackTrace(); } return null; } }
StringRedisTemplate redisTemplate:為spring data為我們提供的redis模板,能操作常用的5中redis資料型別,分別對應如下
redisTemplate.opsForValue();
redisTemplate.opsForHash();
redisTemplate.opsForList();
redisTemplate.opsForSet();
redisTemplate.opsForZSet();
基本都提供了增刪改查的方法,會使用jedis操作redis那麼這個redisTemplate使用也不會是問題。
5、redis過期監聽器
@Service(value=OrderRedisListener.SERVICE_NAME) public class OrderRedisListener implements MessageListener{ public static final String SERVICE_NAME="com.scu.listener.OrderRedisListener"; @Autowired private PayOrderRepository payOrderRepository; @Autowired private GoodsSellsRepository goodsSellsRepository; private static Log log = LogFactory.getLog(OrderRedisListener.class); @Override @Transactional public void onMessage(Message message, byte[] pattern) { //獲取過期的key String expireKey = new String(message.getBody()); System.out.println("終於失效了"); log.debug("key is:"+ expireKey); System.out.println(expireKey); //擷取訂單號 String tradeNo = expireKey.substring(expireKey.indexOf(":")+1); Order order = payOrderRepository.findOrderByTradeNo(tradeNo); if(order!=null && order.getStatus()!=1){ //修改訂單支付狀態為失敗 payOrderRepository.setStatusByOrderId(2, order.getId()); GoodsSell goodsSell = goodsSellsRepository.findOrderByOrderId(order.getId()); //修改商品購買狀態為失敗 goodsSellsRepository.setStatusByGoodsId(2, goodsSell.getId()); } } }
6、配置監聽容器
/** * redis監聽容器 * @author 12706 */ @Configuration public class RedisConfig { @Autowired @Qualifier(OrderRedisListener.SERVICE_NAME) private MessageListener messageListener; @Autowired private RedisTemplate redisTemplate; @Bean RedisMessageListenerContainer container(MessageListenerAdapter listenerAdapter) { RedisMessageListenerContainer container = new RedisMessageListenerContainer(); container.setConnectionFactory(redisTemplate.getConnectionFactory()); container.addMessageListener(listenerAdapter, new PatternTopic("[email protected]__:expired")); return container; } @Bean MessageListenerAdapter listenerAdapter() { return new MessageListenerAdapter(messageListener); } }
獲取RedisMessageListenerContainer也可以寫成
@Bean RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory, MessageListenerAdapter listenerAdapter) { RedisMessageListenerContainer container = new RedisMessageListenerContainer(); container.setConnectionFactory(connectionFactory); container.addMessageListener(listenerAdapter, new PatternTopic("[email protected]__:expired")); return container; }
因為連線工廠在spring容器中是已經存在的,如果是自己配置spring data與redis整合是需要自己配置連線工廠,但是spring boot已經幫我們配置好了,所以可以直接注入。
注意:監聽器能監聽到redis中過期的key是有個要求的,必須在redis配置檔案redis.conf裡面設定能夠監聽到key過期事件,配置如下:
引數說明如下:
可以測試一下,開啟兩個視窗,分別啟動redis客戶端連線,
命令: ./redis-cli
視窗1監聽:
視窗2設定key及過期時間10秒
10秒後再看視窗1,監聽到了過期的key
說明配置生效了。
如果用過ActiveMQ的話,會發現其實配置是很類似的,配置消費者的話會配置連線工廠,配置目的地,配置監聽器,配置監聽容器,兩者都是用來監聽訊息的,主要是在onMessage方法裡面進行邏輯處理。
7、Service層處理
public interface OrderService {
public String payOrder(PayOrderRequestVo requestVo);
}
1
2
3
@Service(value=OrderServiceImpl.SERVICE_NAME)
public class OrderServiceImpl implements OrderService{
public static final String SERVICE_NAME="com.scu.service.impl.OrderServiceImpl";
@Autowired
private PayOrderRepository payOrderRepository;
@Autowired
private GoodsRepository goodsRepository;
@Autowired
private UserRepository UserRepository;
@Autowired
private GoodsSellsRepository goodsSellsRepository;
@Autowired
private OrderRedisService orderRedisService;
/*
* 儲存訂單
*/
@Transactional
public String payOrder(PayOrderRequestVo requestVo){
//模擬生成訂單號 (由支付寶或者微信生成)
String tradeNo = UUID.randomUUID().toString().replace("-", "");
//儲存訂單資訊
Order order = new Order();
order.setCreateTime(new Date());
order.setModifiedTime(new Date());
order.setChannel(requestVo.getChannel());
order.setPayment(requestVo.getCount());
order.setTradeNo(tradeNo);
order.setStatus(0);
payOrderRepository.save(order);
//儲存商品支付資訊
GoodsSell goodsSell = new GoodsSell();
goodsSell.setCreateTime(new Date());
goodsSell.setModifiedTime(new Date());
goodsSell.setCount(requestVo.getCount());
goodsSell.setStatus(0);
goodsSell.setOrder(order);
//查詢使用者資訊,關聯商品銷售
User user = UserRepository.findOne(requestVo.getUserId());
goodsSell.setUser(user);
//查詢商品資訊,關聯商品銷售
Goods goods = goodsRepository.findOne(requestVo.getGoodsId());
goodsSell.setGoods(goods);
goodsSellsRepository.save(goodsSell);
//資訊存入redis的物件
OrderRedisDo orderRedisDo = new OrderRedisDo();
//複製部分屬性
BeanUtils.copyProperties(order,orderRedisDo);
orderRedisDo.setGoodsId(requestVo.getGoodsId());
//儲存資訊
orderRedisService.saveOrder(tradeNo, orderRedisDo);
return tradeNo;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
其中存入redis中的實OrderRedisDo類如下:
public class OrderRedisDo {
private Long id;//訂單id
private Integer payment;//付款金額
private Integer channel;//支付方式
private Integer status;//支付狀態
private Long goodsId;//商品id
private String tradeNo;//訂單號
get、set方法
}
1
2
3
4
5
6
7
8
9
10
8、Controller
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping("/order/pay")
public ResponseTemplate payOrder(@RequestBody PayOrderRequestVo requestVo){
String tradeNo = orderService.payOrder(requestVo);
return new ResponseTemplate(tradeNo);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
@RestController註解相當於@[email protected]+..,返回的是個json。
ResponseTemplate是個返回資訊模板
/**
* 返回資訊模板
* @author 12706
*/
public class ResponseTemplate {
private int code;
private String message;
private List<String> errors;
private Object data;
public ResponseTemplate(Object data) {
super();
this.data = data;
}
public ResponseTemplate() {
super();
}
get、set方法
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
為了看的直觀,目錄結構還是截了下來
測試:啟動專案(執行SpringDataJpaApplication的main方法)
資料庫logistic會多出來4張表,手動分別新增一條記錄到使用者表和商品表中,用來測試。
假如id為1的使用者(Tom)購買了id為1的商品(張飛牛肉),然後就會下單,但他一直沒支付。那麼兩分鐘後該訂單就失效(支付狀態為2,失敗)。
使用postman模擬資料傳送請求。
檢視資料庫表,訂單表和商品銷售表都產生了記錄,且狀態為0(待支付)。
過兩分鐘再去檢視,發現訂單失效(支付狀態為2)
控制檯輸出:
sql語句
終於失效了
order:5a8378f66b4e473cba5f4014a813810e
sql語句